前端单元测试-Jest使用记录

jest:https://github.com/facebook/jest
jest文档:https://jestjs.io/zh-Hans/docs/configuration

2021.12.21 星期二 :

文件结构设计

Jest 推荐你在被测试代码的所在目录下创建一个 __tests__ 目录,但你也可以为你的测试文件随意设计自己习惯的文件结构。不过要当心 Jest 会为快照测试在临近测试文件的地方创建一个 __snapshots__ 目录。

– 单测文件或者tests目录跟要测试的文件放在同个目录,方便我们查找

实际经验

1-1 统一的测试内容,比如shallow,mount等,都放在了test目录下,和源码分离。
1-2 业务相关或定制测试内容,放在源码目录中,有些测试内容并不生成snapshots
1-3 utils(数量可见)还是放在了test目录下,未和源码在一起。不像components,pages等添加/修改随意。
2-1 test目录下放置所有测试相关的内容,包括测试用例,mocks,jest配置文件等。
3-2 但是mocks数据还是放在了src目录下,用.mock 区分。

插件

awesome-jest:https://github.com/jest-community/awesome-jest#results-processors

环境

jest-dom: Custom Jest matchers to test the dom structure.

NOTE: 从jest28开始,默认情况不再提供’Jest环境jsdom’, 请确保单独安装它。

result

报告:
jest-html-reporters 比 jest-html-reporter 强一扭扭:
有dashboard,可以看到图表。列表有分组/折叠。直接跳转到lcov-report,查看每个文件的细节。

https://github.com/dkelosky/jest-stare

This is a Jest HTML reporter. It takes summary test results from jest and parses them into an HTML file for improved readability and filtering.

majestic Zero config UI for Jest.
jest-bamboo-formatter: https://github.com/adalbertoteixeira/jest-bamboo-formatter

配置

npx jest --showConfig > test/jest.conf.output.json
onlyChanged: 只是针对修改的测试用例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"automock": false,
"cacheDirectory": "/private/var/folders/84/q_vxngn51816smpv9prjnx6h0000gp/T/jest_dy",

"injectGlobals": true,

"onlyFailures": false,

"onlyChanged": false,
"lastCommit": false,
--changedSince

"maxConcurrency": 5,

"maxWorkers": 9,

SMTC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// bail: 1,
preset: "ts-jest",
// verbose: true,
globals: {
"ts-jest": {
tsConfig: "<rootDir>/tsconfig.json",
importHelpers: true
}
},
testEnvironment: "jsdom",
rootDir: path.resolve(__dirname, "../"),

// 在jest inital之前执行,比setupFilesAfterEnv更早,例如可以设置全局变量到global
"setupFiles": [
// jsdom,不涉及dom可不引入
"react-app-polyfill/jsdom"
],
// 类似,在jest inital之后执行,这一步能拿到jest的api进行扩展(故名AfterEnv)
// 可以执行例如引入enzyme配置等
"setupFilesAfterEnv": [
"<rootDir>/src/setupTests.js"
],

moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
moduleNameMapper: {
'@tarojs/taro': '<rootDir>/test/mocks/taroMock.js', // tarojs mock
'@tarojs/components': '@tarojs/components/dist-h5/react',
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
"<rootDir>/test/mocks/fileMock.js",
"\\.(css|less|scss)$": "<rootDir>/test/mocks/styleMock.js",
'@/utils/Cookie': '<rootDir>/src/utils/Cookie/index.h5.js',
'@/utils/Track': '<rootDir>/test/mocks/emptyMock.js',
'^@/(.*)$': '<rootDir>/src/$1', // webpack alias 配置
},

testMatch: ["<rootDir>/test/**/*.spec.js", "<rootDir>/test/**/*.spec.jsx"],
testPathIgnorePatterns: [
"/node_modules",
"test/components/Dialog.spec.js*",
"test/pages/balance_confirm*",
],

"transform": {
// 用 `babel-jest` 处理 js
"^.+\\.js$": "<rootDir>/node_modules/babel-jest"
},

// 快照的序列化工具 || 否则测通过,但是快照文件没有内容生成。
snapshotSerializers: ["enzyme-to-json/serializer"],
// "snapshotSerializers":["jest-serializer-vue"],


collectCoverage: true,
coverageDirectory: "<rootDir>/test/coverage",

/**
* 可以用一个 通配模式 的数组来指出仅哪些文件需要收集覆盖率信息。
*
* 1)lcov-report 目录结构是根据这个配置生成的。
* 比如:同时配置 src/** 和src/utils 会生成两个utils的目录。
* src/** /*.ts?(x) 和 src/** /*.ts?(x) 是通一个目录 (ext: 空格是防止注释)
* 2)路径后面必须有`**`, 比如`!/src/enums/**`
*/
collectCoverageFrom: [
// "<rootDir>/src/utils/**",
// 'src/**/!(*.d).{js,ts}',
"<rootDir>/src/**/*.[jt]s?(x)",
"!/src/enums/**",
"!src/**/*-mock*",
],
/**
* An array of regexp pattern strings that are matched against all file paths before executing the test.
* 执行测试前 忽略掉的文件。
* 即不会运行test, 区别于上面的collectCoverageFrom。
* 防止某些文件输出意外的error等信息。
* `STACK: SyntaxError: /Users/user123/myprojects/xxx/pagev32614-mock.js:`
* collectCoverageFrom 和 coveragePathIgnorePatterns 都可以生效。
*/
coveragePathIgnorePatterns: [
'url-decode\\.json',
'.*-mock\..*',
],

/**
* clover -> clover.xml;
* json -> `coverage-final.json`;
* lcov -> lcov-report/目录, Icov.info; 每一个测试用例/文件的结果报告(详细)。
* text: 控制台视图.
* Icov.info 文件记录了一堆信息。
*/
"coverageReporters": [/* "clover", */ "json", "lcov", ["text", {"skipFull": true}]],

/**
* 平台报告解析,需要配置JEST_REPORT_FILE路径。
jest-bamboo-formatter: https://github.com/adalbertoteixeira/jest-bamboo-formatter/issues
package.json
*
       "test": "cross-env JEST_REPORT_FILE='./test/report/test-report.json' jest --config test/jest.conf.js"
*   
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 */
testResultsProcessor: "jest-bamboo-reporter",
// "coverageThreshold": {
// "global": {
// "branches": 50,
// "functions": 50,
// "lines": 50,
// "statements": 50
// },
// "./src/components/": {
// "branches": 40,
// "statements": 40
// },
// },

reporters: [
"default",
[
"./node_modules/jest-html-reporter",
{
pageTitle: "Test Report",
outputPath: "./test/report/test-report.html"
}
],
["jest-html-reporters", {
"publicPath": "./test/report",
"filename": "jest_html_reporters.html",
"openReport": true
}]
]

globals, globalSetup; setupFiles, setupFilesAfterEnv

小程序配置

小程序-自定义组件-单元测试
miniprogram-simulate: https://github.com/wechat-miniprogram/miniprogram-simulate

1
2
3
4
5
6
7
{
// jest 是直接在 nodejs 环境进行测试,使用 jsdom 进行 dom 环境的模拟。在使用时需要将 jest 的 `testEnvironment` 配置为 `jsdom`。
// jest 内置 jsdom,所以不需要额外引入。
"testEnvironment": "jsdom",
// 配置 jest-snapshot-plugin 从而在使用 jest 的 snapshot 功能时获得更加适合肉眼阅读的结构
"snapshotSerializers": ["miniprogram-simulate/jest-snapshot-plugin"]
}

问题

  1. getApp is not defined

    我们可以通过jest提供的global设置全局变量,可以在测试文件中单独编写,或者在package.json的jest块设置setupFiles属性,让jest自动加载。

由于小程序页面的构成包含Page和生命钩子等miniprogram-simulate不提供的能力,所以我们需要自己先进行全局配置Page。

1
2
3
4
5
6
{
globals: {
getApp: () => ""
},
"setupFiles": ["./setup.js"]
}

\$_PS: 但是实际中,通过globals 只有一个用例单独测试的时候可以通过。两个用例一起测试又回报错。??不解。

snapshots 会有变化,因为有数据了。覆盖率也有提升。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/**
* ## 1 直接在测试用例中mock(单个)接口
*/
jest.mock('@tarojs/taro')
// Taro.request.mockResolvedValue(mockData);
Taro.request.mockImplementation((requests) => {
const { url, data, method, header } = requests || {}
let result = { code: '0', body: {}, message: 'mock data' }
if (url.includes('api.m.jd.com/client.action')) {
result = mockData
}
return Promise.resolve(result)
});
/**
* ## 2 保留taro其他模块的mock
* jest.requireActual
*/
jest.mock('@tarojs/taro', () => {
const originalModule = jest.requireActual('../../mocks/taroMock');
return {
__esModule: true,
...originalModule,
request: jest.fn().mockImplementationOnce((requests) => {
// console.log('mocks Implementation', requests)
const { url, data, method, header } = requests || {}
let result = { code: '7890', body: {}, message: 'mock data' }
if (url.includes('api.m.jd.com/client.action')) {
const functionId = data.functionId
// console.log('mocks Implementation. functionId', functionId)
if (functionId.includes('get_address_detail')) {
result = mockData
}
}
return Promise.resolve(result)
}),
foo: 'mocked foo',
}
});
/**
* ## 3 分离测试用例和数据mock
* 1) 在外层taro.request mock的时候处理返回数据,从用例中分离数据mock逻辑。
* 2) 并且分离taro mock 和api数据mock
* 3) 在Implementation 根据不同的请求接口去处理数据。
* 通过mockImplementation,对不同的方法(url等)返回不同的数据。
*/
/* test/mocks/apiMock.js */
// api Mock 数据模块路径。目前仅mock主接口。
const apiMockModule = {
order_list: 'list',
order_detail: 'detail',
get_logistics_list_by_order: 'follow',
}
/**
* 处理Taro.requests 请求mock
* @param {*} request
* @returns
*/
const handleApiMock = function(requests) {
// console.log('mocks Implementation', requests)
const { url, data, method, header } = requests || {}
let result = { code: '7890', body: null, message: 'mock data' }
if (url.includes('api.m.com/client')) {
const functionId = data.functionId.replace(/_m$/g, '')
const mockItem = apiMockModule[functionId]
if (mockItem && Object.keys(apiMockModule).includes(functionId)) {
result = require(`@/mocks/${mockItem}.mock`).default
}
}
return Promise.resolve(result)
}
export default handleApiMock
/* test/mocks/taroMock.js */
export const request = jest.fn().mockImplementation(handleApiMock);

库:jest-location-mock
或者4中方式:1 delete;2 Object.defineProperty;3 mockFile; 4 spyOn



1
2
3
4
5
6
7
8
9
10
11
12
13
import Comp from '../../src/pages/detail';

describe('Comp Snapshot', () => {
it('shallow:: [src/pages/detail]should be correct.', () => {
const wrapper = shallow(<Comp />);
expect(wrapper).toMatchSnapshot();
});
it('mount:: [src/pages/detail]should be correct.', async () => {
let wrapper;
await act(async () => wrapper = mount(<Comp />));
expect(wrapper).toMatchSnapshot();
});
});

## Why do I get this error?
In test, the code to render and update React components need to be included in React’s call stack. So the test behaves more similar to the user experience in real browsers.

However, if your test still complains about “not wrapped in act(…)”, you might encounter one of these 4 cases described below.
Case 1: Asynchronous Updates
Case 2: Jest Fake Timers
Case 3: Premature Exit
Case 4: Formik Updates

## Conclusion
In test, React needs extra hint to understand that certain code will cause component updates. To achieve that, React-dom introduced act API to wrap code that renders or updates components. React testing library already wraps some of its APIs in the act function. But in some cases, you would still need to use waitFor, waitForElementToBeRemoved, or act to provide such “hint” to test.

<!–
其他:
Fix the “not wrapped in act(…)” warning
PS: 视频+代码演示说明。

How to solve the “update was not wrapped in act()” warning in testing-library-react?

No need for the waitFor or waitForElement. You can just use findBy* selectors which return a promise that can be awaited. e.g await findByTestId(‘list’);
Wait until the mocked get request promise resolves and the component calls setState and re-renders. waitForElement waits until the callback doesn’t throw an error
–>

  1. TypeError: Cannot read property 'propTypes' of undefined 和单测没有关系。单测检测出来的结构赋值有问题。定位到位置,然后修改。
knowledge is no pay,reward is kindness
0%